function hFig = StereoAnalyzerRT(x, y, Fs)
%StereoAnalyzerRT 立体声 XY 数字荧光作图. Stereo X-Y Plot with Digital Phosphor
%  hFig = StereoAnalyzerRT(x, y, Fs)
%  
%  绘制有数字荧光余晖的双通道立体声的 X-Y 图并实时播放.
%  输入以 Fs 采样的 x, y 左右声道数据, 返回窗口句柄.
%
%  x, y 应当是等长实数向量, 元素值在 ±1 内. 为保证流畅, 建议降采样
%  到22.050ksps 或更低. 实时回放只使用左声道 x 数据.
%  
%  by adq @ XJTU, 2022-04-06, rev 1
%  
%  rev 1
%  增加参数检测
%  可使用滚动条在线调节荧光亮度和衰减.
%  状态显示加入播放进度和等效回放率数据.
%  显著改善音画同步和性能:
%  - 使用已经封装好的 sound 替代 audioplayer;
%  - 利用分段, 降低音频设备和 fps 记录刷新率, 降低用户控件中断频率;
%  调节代码风格, 文件内文档和窗口颜色.
%  
%  rev 0
%  实验了基本功能.
%  尝试使用 audioplayer 对象以同步播放音频, 性能很差(FPS<5).
%  
status = struct('debug', false, 'inLoop', true, ...
	'closeAtFinish', true);

assert(isvector(x) && isreal(x) && ~issparse(x) && ...
	isvector(y) && isreal(y) && ~issparse(y), ...
	'StereoAnalyzerRT:NoRealVectors', '输入必须是实数向量');
assert(length(x) == length(y), ...
	'StereoAnalyzerRT:UnequalVectorLength', '输入向量必须等长');
assert(isscalar(Fs) && isnumeric(Fs), ...
	'StereoAnalyzerRT:NoNumericScalar', '输入采样率必须是数值标量');

%% 参数设置

segCount = 8; % 每经过 segCount 次绘制后更新统计, 音频, 和用户设置

% 数字荧光参数

xRange = [-1 1]; % 输入信号的范围
yRange = xRange;

xRes = 144; % 输出分辨率
yRes = xRes;

% 亮度累加按照格子进行, 下面参数的效果与 x/yRes, 单次刷新点数 segLen 均有关

segLen = ceil(44100/32); % 单次刷新点数, 必须为整数
	
% pausems = 0.002 * sqrt(segLen);
pausems = 0.061;
	
% 单次刷新后暂停时间. 最低 0.001s, 否则不能响应用户 GUI
% segLen 越高, 这个值应该越大

imgInterp = 'bilinear'; % bilinear nearest. 对性能影响微乎其微
% 应当使荧光数据过饱和, 效果接近示波器. 使用 uint8 256 级颜色和颜色图
phosphorIntensity = .12 * sqrt(xRes);
% 尺寸越大, 一个像素所占据比例越小, 则其亮度应该增加
	
% phosphorDecay = 1 - .05 .* log(segLen);
phosphorDecay = 1 - .17 .* log(segLen);
phosphorDecay = max([phosphorDecay, .2]);
	
% 	phosphorIntensity = 1.1;
% 	phosphorDecay = .6;
	
% 单次刷新点数越低, 需要暂留时长越高, 否则屏幕上只有扫描点
% 反之, 单次刷新点多, 能够一次或多次扫描整个波形, 应调小, 否则有拖影

sampleLen = length(x);
segBaseIdx = .0; % 开始播放的位置
segBaseIdx = ceil(segBaseIdx*sampleLen);
	
%% 坐标转换计算部分
% 	coordinate conversion: floatpoint [lb, ub] to integer [1, resMax]. n=k*x+b
xSlope = (xRes - 1) / (xRange(2) - xRange(1));
xBias = - xSlope * xRange(1);

ySlope = (yRes - 1) / (yRange(2) - yRange(1));
yBias = - ySlope * yRange(1);

persistence = zeros(xRes, yRes, 'single');
	
%% 建立主窗口对象

disp('Building main window.');

hFig = figure('Visible', 'off', 'Name', 'Stereo Analyzer Real Time', ...
	'WindowStyle', 'normal', 'Units', 'pixels', 'Position', [0,0,1100,640], ...
	'Pointer', 'crosshair', 'SelectionType', 'extend', ...
	'GraphicsSmoothing', 'off','DeleteFcn', @figDelete);
% 	hF.WindowStyle = 'docked'; 'OuterPosition', [0,0,1100,720]

if status.debug
	hFig.ToolBar = 'figure'; hFig.MenuBar = 'figure'; hFig.NumberTitle = 'on';
else
	hFig.ToolBar = 'none'; hFig.MenuBar = 'none'; hFig.NumberTitle = 'off';
end
	
%% 建立控制面板 Control Panel 元素
	
a = .05; % layout unit, 横纵各划分 20 份

hPnlCtrl = uipanel('Parent', hFig, ...
	'Units','normalized', 'OuterPosition', a.*[0, 0, 5, 20], ...
	'Title', 'Control Panel', 'FontName', 'Courier');
% 是 panel 的 children, 其相对尺寸以 panel 为基准而非整个 figure. 下同

tutuColor1 = [.2 .4 .6];
tutuColor2 = [1 .78 .21];
hPnlCtrl.BackgroundColor = tutuColor2;

hTxtStatus = uicontrol('Parent', hPnlCtrl, 'Style', 'text', ...
	'Units','normalized', 'OuterPosition', a.*[0, 17, 20, 3], ...
	'HorizontalAlignment', 'left', ...
	'FontName', 'Courier', 'BackgroundColor', tutuColor2);
% 'BackgroundColor', [1 1 1], 'ForegroundColor', [0 0 0], 'FontName', 'FixedWidth'

% hEditFPS = uicontrol('Parent', hPnlCtrl, 'Style', 'edit', ...
% 	'Units','normalized', 'OuterPosition', a.*[0, 16, 20, 1], ...
% 	'HorizontalAlignment', 'left', ...
% 	'FontName', 'Courier', 'String', 'Max FPS');

hBtnPause = uicontrol('Parent', hPnlCtrl, ...
	'Style', 'Pushbutton', 'String', 'Pause',...
	'Units', 'normalized', 'Position', a.*[0, 2, 20, 2], ...
	'FontName', 'Courier', 'Tooltip', 'Suspend execution.', ...
	'Callback', @btnPauseCallback, 'Interruptible', 'off', 'BusyAction', 'cancel');

hBtnExit = uicontrol('Parent', hPnlCtrl, ...
	'Style', 'Pushbutton', 'String', 'Exit', ...
	'Units', 'normalized', 'OuterPosition', a.*[0, 0, 20, 2], ...
	'FontName', 'Courier', 'Tooltip', 'Terminate execution.', ...
	'Callback', @btnExitCallback, 'Interruptible', 'off', 'BusyAction', 'cancel');

hSlideIntensity = uicontrol('Parent', hPnlCtrl, 'Style', 'slider', ...
	'Units', 'normalized', 'OuterPosition', a.*[0, 4, 20, 1], ...
	'FontName', 'Courier', 'Tooltip', 'Fluorescent Intensity', ...
	'Value', phosphorIntensity, 'Max', .5.*sqrt(xRes), 'Min', 0);

hTxtIntensity = uicontrol('Parent', hPnlCtrl, 'Style', 'text', ...
	'Units','normalized', 'OuterPosition', a.*[0, 5, 20, .7], ...
	'HorizontalAlignment', 'left', 'FontName', 'Courier', ...
	'String', 'Fluorescent Intensity', 'BackgroundColor', tutuColor2);

hSlideDecay = uicontrol('Parent', hPnlCtrl, 'Style', 'slider', ...
	'Units', 'normalized', 'OuterPosition', a.*[0, 6, 20, 1], ...
	'FontName', 'Courier', 'Tooltip', 'Fluorescent Decay', ...
	'Value', phosphorDecay, 'Max', 1, 'Min', .2);

hTxtDecay = uicontrol('Parent', hPnlCtrl, 'Style', 'text', ...
	'Units','normalized', 'OuterPosition', a.*[0, 7, 20, .7], ...
	'HorizontalAlignment', 'left', 'FontName', 'Courier', ...
	'String', 'Fluorescent Decay', 'BackgroundColor', tutuColor2);

%% 建立绘图面板 Plot Panel 元素

hPnlPlot = uipanel('Parent', hFig, ...
	'Units','normalized', 'OuterPosition', a.*[5, 0, 15, 20], ...
	'Title', 'Plot Panel', 'FontName', 'Courier', 'BackgroundColor', tutuColor1);

% hPnlPlot.BackgroundColor = .45.*[1 1 1];

axXLim = [1 xRes]; axXTick = linspace(1, xRes, 11);
axYLim = axXLim; axYTick = axXTick;
% axYLim = [1 yRes]; axYTick = linspace(1, yRes, 11);
axXTickLabel = num2str(linspace(xRange(1), xRange(2), 11)', '%+04.1f');
axYTickLabel = axXTickLabel;
% axYTickLabel = num2str(linspace(yRange(1), yRange(2), 11)', '%+04.1f');
% num2str, 行向量转换为空格分隔的一个字符串, 列向量转换为字符串数组

tickColor = .9.*[1 1 1];

hAx = axes('Parent', hPnlPlot, ...
	'Units', 'normalized', 'OuterPosition', a.*[2, 0, 18, 20], ...
	'NextPlot', 'add', 'SortMethod', 'childorder', 'Clipping', 'off', ...
	'XLim', axXLim, 'YLim', axYLim, 'XTick', axXTick, 'YTick', axYTick, ...
	'XTickLabel', axXTickLabel, 'YTickLabel', axYTickLabel, ...
	'TickDir', 'out', 'Box', 'on', ...
	'XGrid', 'on', 'YGrid', 'on', 'GridAlpha', 1, ...
	'Color', 'none', 'XColor', tickColor, 'YColor', tickColor, ...
	'FontName', 'Courier', 'FontSmoothing', 'off', ...
	'FontWeight', 'bold');
% 'Color' 背景颜色 绘图在 image 对象上进行, 不需要在画布上色
xlabel('X');
ylabel('Y');

% 辅助线
% 	plot(hAx,[0 xRes], yRes/2.*[1 1], 'Color', 190 / 256 .* [1 1 1]);
% 	plot(hAx, xRes/2.*[1 1], [0 yRes], 'Color', 190 / 256 .* [1 1 1]);
plot(hAx, [0 xRes], [yRes 0], 'Color', tickColor);
plot(hAx, [0 xRes], [0 yRes], 'Color', tickColor);

im = image(hAx, persistence', 'AlphaData', .8);
im.Interpolation = imgInterp;
im.SelectionHighlight =	'off';
% axis equal
global fluoGreen
colormap(fluoGreen); % hot gray bone pink copper
% 	colorbar;

	hFig.Visible = 'on';
	disp('Window is now ready.');
	
%% 初始化时间和帧率计算, 循环运行
	
fpsData = NaN .* ones(1, segCount);
frameSec0 = posixtime(datetime('now'));
frameSec1 = frameSec0;

frameCounter = segCount;

while status.inLoop

	% 每帧计算 FPS, segCount 帧后输出均值, 播放新音频片段, 更新用户设置

	if frameCounter >= segCount

		frameCounter = 0;
		fpsAvg = mean(fpsData, 'omitnan');
		hTxtStatus.String = ...
			sprintf('%.1f FPS\n%.0f SPS\nProgress: %0.3f\n\nIntensity: %.3f\nDecay: %.3f', ...
			fpsAvg, fpsAvg*segLen, segBaseIdx/sampleLen, phosphorIntensity, phosphorDecay);

		phosphorIntensity = hSlideIntensity.Value;
		phosphorDecay = hSlideDecay.Value;

		if segBaseIdx+segLen*segCount > sampleLen, continue; end

		sound(x(segBaseIdx+1:1:segBaseIdx+segLen*segCount), Fs);
	end

	frameSec1 = posixtime(datetime('now'));
	fps = 1 / (frameSec1 - frameSec0);
	frameSec0 = frameSec1;
	fpsData(frameCounter+1) = fps;

	frameCounter = frameCounter + 1; 

	idxs = segBaseIdx+1:1:segBaseIdx+segLen;

	% coordinate conversion
	i = uint16(xSlope * x(idxs) + xBias);
	j = uint16(ySlope * y(idxs) + yBias);

	% 抗饱和 1: 超范围的点绘制到边框
	i(i>xRes) = xRes; i(i<1) = 1;
	j(j>yRes) = yRes; j(j<1) = 1;
% 	persistence(i, j) = persistence(i, j) + phosphorIntensity;
% 注意,  如果 (i,j) 坐标有重复, 如 ta([1 1 1], [1 1 2])=ta([1 1 1], [1 1 2])+1, 会导致 ta(1,1) 只被加一次(希望是2次).
% 这可能造成拖影(绘制出矩形区域)

	for idx = 1:segLen
% 		if i(idx) > xRes || i(idx) < 1 || j(idx) > yRes || j(idx) < 1, continue, end
		% 抗饱和 2: 超范围的点不绘制
		persistence(i(idx), j(idx)) = persistence(i(idx), j(idx)) + phosphorIntensity;
	end

	% im.CData = uint8(256.*persistence./max(persistence)); 错. max(max(per)) 对
	im.CData = uint8(256 .* rot90(persistence', 2));
	% 饱和限幅: 直接画; 动态对比度: 类似 imagesc, 最大值归一化. 前者更接近实际情况
	% 注意 image 的绘制顺序, 需要转置后才和左下角原点的坐标系相同. rot90(persistence', 2)

	persistence = persistence .* phosphorDecay;
	segBaseIdx = segBaseIdx + segLen;

	if segBaseIdx + segLen >= sampleLen, status.inLoop = false; end

	pause(pausems);

end

if status.closeAtFinish, figDelete('caller', 'event'); end

%% 函数定义

function figDelete(~, ~)
	try
		status.flagRunLevel = 0;
		delete(hFig);
		disp(fpsAvg);
		disp('App has been terminated.');
	catch
		% suppress warning when deleting elements
	end
end

function btnPauseCallback(~, ~)
	disp('App halted by user.');
% 		fprintf('FPS: %.1f\nAvg: %.1f\n', fps, fpsAvg);
	uiwait(warndlg('App halted by user.', 'Warning'));
end

function btnExitCallback(~, ~)
	figDelete('caller', 'event');
end

end